commit - cb06e3c7bfba13d9d84df89000c89541a787cc1a
commit + 0c159575fef3ab03ef66215753887d4044a22f3c
blob - /dev/null
blob + dcd85bd6b12bdb29d546551e39f421523d38466d (mode 644)
--- /dev/null
+++ twisource/LICENSE
+The MIT License (MIT)
+
+Copyright (c) 2015 Sergey Bronnikov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
blob - /dev/null
blob + 201ef5c4b2b8d40eb1684ab7a6225f3f10a002fc (mode 644)
--- /dev/null
+++ twisource/README.md
+<img width=150 src="tweets.png" align="right" />
+
+Twisource
+=========
+
+- is a Twitter client. It allows you to update status on Twitter from source
+file.
+
+## Installation
+
+- Install Python requirements
+
+```
+$ pip install -r requirements.txt
+```
+
+- Add tweets to tweets.yml.
+- Create file(s) with account(s) credentials.
+- Validate source file with scheduled tweets:
+
+```
+$ ./twisource
+```
+
+- Commit updated file and publish tweets:
+
+```
+$ git commit -a
+$ git push
+$ ./twisource --publish
+```
+
+## Getting access tokens
+
+Has to be done once.
+
+1. Register your application via <https://apps.twitter.com/>.
+2. Get your consumer and access tokens from
+ <https://apps.twitter.com/app/[APP_ID]/keys>.
+ Here you may have to confirm your phone number, only then Twitter will allow
+ you to update statuses within an application.
+3. Save `consumer_key`, `consumer_secret`, `access_token`,
+ `access_token_secret`.
+
+## Users
+
+- [OpenVZ, CRIU, P.Haul](https://www.openvz.org)
+- ...
+
+## Similar tools
+
+- [publishr](https://github.com/vti/publishr)
+- [twty](https://github.com/mattn/twty)
+
+## Contacts
+
+[sergeyb@](https://twitter.com/estet)
blob - /dev/null
blob + 8752d21ecf190e04a1fea92c86924f76883d093b (mode 644)
--- /dev/null
+++ twisource/main.go
+// Twitter https://github.com/kurrik/twittergo
+// Twitter https://github.com/ChimeraCoder/anaconda
+// Facebook https://github.com/huandu/facebook
+// Tumblr https://github.com/mattcunningham/gumblr
+// Instagram https://github.com/yanatan16/golang-instagram
+// Reddit https://github.com/jzelinskie/geddit
+// G+ https://godoc.org/google.golang.org/api/plus/v1
+// IRC https://github.com/sorcix/irc
+
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "github.com/kurrik/oauth1a"
+ "github.com/kurrik/twittergo"
+ "gopkg.in/yaml.v2"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+type secret struct {
+ AccessSecret string
+ AccessToken string
+ ClientSecret string
+ ClientToken string
+}
+
+type Tweet struct {
+ Account string
+ Text string
+ Date string
+}
+
+const (
+ TZ = "Europe/Moscow"
+ FILE = "tweets.yml"
+ interval = 5
+ timeFormat = "2006-01-02 15:04"
+)
+
+var (
+ client *twittergo.Client
+ req *http.Request
+ resp *twittergo.APIResponse
+ tweet *twittergo.Tweet
+)
+
+func LoadCredentials(account string) (c *twittergo.Client, err error) {
+ var file = "settings-" + account + ".json"
+ creds, err := ioutil.ReadFile(file)
+ if err != nil {
+ log.Printf("%v", err)
+ return nil, err
+ }
+ var s secret
+ err = json.Unmarshal(creds, &s)
+
+ config := &oauth1a.ClientConfig{
+ ConsumerKey: s.ClientToken,
+ ConsumerSecret: s.ClientSecret,
+ }
+
+ log.Printf("DEBUG: AccessSecret %v\n AccessToken %v\n", s.AccessSecret, s.AccessToken)
+ log.Printf("DEBUG: ClientSecret %v\n ClientToken %v\n", s.ClientSecret, s.ClientToken)
+ user := oauth1a.NewAuthorizedConfig(s.AccessToken, s.AccessSecret)
+ client := twittergo.NewClient(config, user)
+ return client, nil
+}
+
+func main() {
+
+ flag.Usage = func() {
+ fmt.Printf("twisource is a Twitter client with collaboration support.\n\n")
+ fmt.Printf("Usage: twisource [options]\n\n")
+ flag.PrintDefaults()
+ }
+
+ var mode = *flag.Bool("publish", false, "mode to publish tweets")
+ flag.Parse()
+
+ if mode {
+ log.Printf("Mode is public.")
+ }
+
+ filename, _ := filepath.Abs(FILE)
+ yamlFile, err := ioutil.ReadFile(filename)
+
+ if err != nil {
+ panic(err)
+ }
+
+ tweets := []Tweet{}
+
+ err = yaml.Unmarshal(yamlFile, &tweets)
+ if err != nil {
+ log.Fatalf("error: %v", err)
+ }
+
+ now := time.Now()
+ for _, t := range tweets {
+ log.Printf(" Text %s\n", t.Text)
+ log.Printf(" Account %s\n", t.Account)
+ log.Printf(" Date %s\n", t.Date)
+ sched, err := time.Parse(timeFormat, t.Date)
+ if err != nil {
+ log.Fatalf("error: %v", err)
+ }
+ if int64(sched.Sub(now).Minutes()) > 5 {
+ log.Println("[DEBUG] Less than 5 min")
+ client, err := LoadCredentials(t.Account)
+ if err != nil {
+ log.Printf("[DEBUG] Failed to load credentials: %v\n", err)
+ }
+
+ data := url.Values{}
+ data.Set("status", fmt.Sprintf("Hello %v!", time.Now()))
+ body := strings.NewReader(data.Encode())
+ req, err = http.NewRequest("POST", "/1.1/statuses/update.json", body)
+ if err != nil {
+ fmt.Printf("Could not parse request: %v\n", err)
+ os.Exit(1)
+ }
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ resp, err = client.SendRequest(req)
+ if err != nil {
+ fmt.Printf("Could not send request: %v\n", err)
+ os.Exit(1)
+ }
+
+ tweet = &twittergo.Tweet{}
+ err = resp.Parse(t.Text)
+ if err != nil {
+ if rle, ok := err.(twittergo.RateLimitError); ok {
+ log.Printf("Rate limited, reset at %v\n", rle.Reset)
+ } else if errs, ok := err.(twittergo.Errors); ok {
+ for i, val := range errs.Errors() {
+ fmt.Printf("Error #%v - ", i+1)
+ fmt.Printf("Code: %v ", val.Code())
+ fmt.Printf("Msg: %v\n", val.Message())
+ }
+ } else {
+ fmt.Printf("Problem parsing response: %v\n", err)
+ }
+ os.Exit(1)
+ }
+ }
+ }
+}
blob - /dev/null
blob + c52eb68082f2e0c9a268925967832be2e4c526f1 (mode 644)
--- /dev/null
+++ twisource/requirements.txt
+tweepy==2.1
+PyYAML==3.11
+argparse
+pytz
blob - /dev/null
blob + af8ac2d3bcf5188efdd67e3862bd10f884ba8812 (mode 755)
--- /dev/null
+++ twisource/settings-estet.json
+{
+ "AccessSecret": "nTtc6nKzKSNtxuNdPgtB7SubM2dddlkalcmlLJlOE8Ik0C4Sz",
+ "AccessToken": "11690312-ZJ1sVHHHTBdeEol7rNO7tKkxsWBgEIwtla2bulwk",
+ "ClientSecret": "sOwsx28hFZS59esb3TgR4vZfDAfsJ1V1FDFmysHkcgKfrcUDuz",
+ "ClientToken": "wRjW0uVyYVoHpyJ1myBxbOaML"
+}
blob - /dev/null
blob + c873986d3e79767dbc33fb28a3a638ef14a64b1f (mode 644)
Binary files /dev/null and twisource/tweets.png differ
blob - /dev/null
blob + fcc913236c3002716a0700ea1c171586e68c6507 (mode 644)
--- /dev/null
+++ twisource/tweets.yml
+---
+
+- account: estet
+ text: "Twisource is a collaboration Twitter client to post tweets from file https://goo.gl/O7bCuf"
+ date: 2015-10-22 20:34
blob - /dev/null
blob + d0527ae651adcf5f16a90432a6da5e07432f33c8 (mode 755)
--- /dev/null
+++ twisource/twisource.py
+#!/usr/bin/env python
+
+# BSD license, Sergey Bronnikov
+
+import argparse
+import os
+import re
+import tweepy
+from pytz import timezone
+import datetime
+import yaml
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+tz = 'Europe/Moscow'
+tweet_file = 'tweets.yml'
+
+
+def lint(message, date, account):
+ if len(message) > 140:
+ print "[DEBUG] The text of your tweet scheduled at", date, "is too long"
+ return 1
+ return 0
+
+ f = "settings-" + account + ".json"
+ if not (os.path.isfile(f) and os.access(f, os.R_OK)):
+ print "[DEBUG] File with credentials is not accessible"
+ return 1
+ return 0
+
+
+def getRTid(message):
+ m = re.match("^RT ([0-9]*)$", message)
+ if m:
+ return m.groups()[0]
+
+
+def cred(account):
+ f = "settings-" + account + ".json"
+ with open(f) as fp:
+ return json.load(fp)
+
+
+def tweeter(message, account, mode):
+ credentials = cred(account)
+ auth = tweepy.OAuthHandler(credentials['ClientToken'], credentials['ClientSecret'])
+ auth.set_access_token(credentials['AccessToken'], credentials['AccessSecret'])
+ api = tweepy.API(auth)
+ rt_id = getRTid(message)
+ if rt_id:
+ print "[DEBUG] %s: RT https://twitter.com/statuses/%s" % (account, rt_id)
+ else:
+ print "[DEBUG] %s: %s" % (account, message)
+ if not rt_id:
+ if mode:
+ print "[DEBUG] Posting a message", message
+ api.update_status(message)
+ print "[DEBUG] Posted"
+ else:
+ if mode:
+ print "[DEBUG] Retweeting a message", rt_id
+ api.retweet(rt_id)
+ print "[DEBUG] Posted"
+
+
+def main(mode):
+ with open(tweet_file, 'r') as f:
+ tweets = yaml.load(f)
+
+ d = datetime.datetime.now(timezone(tz))
+ print "[DEBUG] Time in timezone %s - %s" % (tz, d.strftime("%Y-%m-%d %H:%M"))
+ for t in tweets:
+ if t['date'] >= d.strftime("%Y-%m-%d %H:%M") and not lint(t['text'], t['date'], t['account']) and not mode:
+ tweeter(t['text'], t['account'], mode)
+ if t['date'] == d.strftime("%Y-%m-%d %H:%M") and not lint(t['text'], t['date'], t['account']) and mode:
+ tweeter(t['text'], t['account'], mode)
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Post Twitter messages from file.')
+ parser.add_argument("-p", "--publish", help="post scheduled messages", action='store_true')
+ args = parser.parse_args()
+
+ if args.publish:
+ print "[DEBUG] publish mode enabled"
+ else:
+ print "[DEBUG] lint mode enabled"
+
+ main(mode=args.publish)