Skip to content

Commit 060b561

Browse files
author
Eric Woolard
committed
Initial commit
0 parents  commit 060b561

File tree

6 files changed

+408
-0
lines changed

6 files changed

+408
-0
lines changed

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
New-Modmail Fetcher
2+
------------
3+
This was created using the bare Reddit API for new-modmail, since at the time PRAW had yet to update.
4+
The new-modmail API is pretty messy, so I did the best I could with what I gots.
5+
6+
INFO
7+
----------
8+
It currently runs every 2 minutes, checks the most recent 10 modmails in the 'Archived' section of modmail,
9+
and writes the response to a cached json file. It then goes through the cached copy, grabs info for each
10+
conversation and logs a new document in the modmail.new collection in mongo. Since individual message replies
11+
to a particular modmail conversation are not accessible from the initial request, it also makes a separate
12+
request to the `/conversations/{ConversationID}` endpoint to retrieve individual responses for each modmail
13+
conversation stored in the cached response. If it finds a conversation by ID that already exists in the
14+
mongodb collection, it updates it with any new messages.
15+
16+
One downside to the new-modmail API is that the response does not include a key for a created_at value.
17+
Instead, we're given times for `lastUpdated`, `lastUserUpdate` and `lastModUpdate`. This script uses
18+
`lastUpdated`, so if a modmail conversation gets a new message, that timestamp also gets updated.

cfg.py

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# System imports
2+
from time import time
3+
# Third-party imports
4+
import praw
5+
# Project imports
6+
import file_manager
7+
8+
r = None
9+
config_path = 'config/' # Root of all configuration files
10+
last_updated = 0
11+
cached_returns = {} # Used to, well, cache function returns. Not wildly
12+
13+
14+
# used in the configuration files, but a handy
15+
# option nonetheless.
16+
# TODO: Look into streamlining the caching process
17+
18+
# Aliases of the file_manager functions to control the relative path
19+
def read(relative_path):
20+
global config_path
21+
return file_manager.read(config_path + relative_path)
22+
23+
24+
def readJson(relative_path):
25+
global config_path
26+
return file_manager.readJson(config_path + relative_path)
27+
28+
29+
def save(relative_path, data):
30+
global config_path
31+
return file_manager.save(config_path + relative_path, data)
32+
33+
34+
def saveJson(relative_path, data):
35+
global config_path
36+
return file_manager.saveJson(config_path + relative_path, data)
37+
38+
39+
# Bot settings
40+
def getSettings():
41+
import authentication
42+
global cached_returns
43+
global last_updated
44+
global config_path
45+
global r
46+
global profile
47+
48+
# Only allow updating the settings once every couple minutes
49+
if 'settings' in cached_returns and time() - cached_returns['settings']['updated'] < 60 * 2:
50+
return cached_returns['settings']['return']
51+
52+
settings = readJson('settings.json')
53+
54+
# Get the OAuth information
55+
settings['bot'] = getAccounts()
56+
57+
if 'settings' not in cached_returns:
58+
cached_returns['settings'] = {}
59+
cached_returns['settings']['updated'] = time()
60+
cached_returns['settings']['return'] = settings
61+
62+
return settings
63+
64+
65+
# Bot account OAuth information // These don't follow the profile,
66+
# keep all bots in the same accounts.json file
67+
def getAccounts():
68+
global config_path
69+
return file_manager.readJson(config_path + 'accounts.json')
70+
71+
72+
def setAccounts(newAccounts):
73+
global config_path
74+
file_manager.saveJson(config_path + 'accounts.json', newAccounts)
75+
76+
77+
# The Reddit app OAuth info necessary for registering ourselves as legit
78+
def getOAuthInfo():
79+
return readJson('oauth.json')

config/output_example.json

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"conversationIds": [],
3+
"viewerId": "",
4+
"conversations": {
5+
"id": {
6+
"lastUpdated": "",
7+
"subject": "subject",
8+
"authors": [
9+
{
10+
"isDeleted": false,
11+
"isOp": true,
12+
"isParticipant": false,
13+
"name": "",
14+
"isHidden": false,
15+
"isMod": true,
16+
"isAdmin": false,
17+
"id":
18+
}
19+
],
20+
"state": 2,
21+
"isRepliable": true,
22+
"isAuto": false,
23+
"id": "",
24+
"isInternal": false,
25+
"owner": {
26+
"type": "subreddit",
27+
"displayName": "GlobalOffensive",
28+
"id": "t5_2sqho"
29+
},
30+
"objIds": [
31+
{
32+
"key": "messages",
33+
"id": ""
34+
}
35+
],
36+
"lastUnread": null,
37+
"numMessages": 1,
38+
"isHighlighted": false,
39+
"lastUserUpdate": null,
40+
"lastModUpdate": "",
41+
"participant": {
42+
"isDeleted": false,
43+
"isOp": false,
44+
"isParticipant": true,
45+
"name": "",
46+
"isHidden": false,
47+
"isMod": false,
48+
"isAdmin": false,
49+
"id":
50+
}
51+
}
52+
},
53+
"messages": {
54+
"id": {
55+
"body": "",
56+
"isInternal": false,
57+
"bodyMarkdown": "",
58+
"author": {
59+
"isDeleted": false,
60+
"isOp": false,
61+
"isParticipant": false,
62+
"name": "",
63+
"isHidden": false,
64+
"isMod": true,
65+
"isAdmin": false,
66+
"id":
67+
},
68+
"date": "2017-03-26T12:44:21.793602+00:00",
69+
"id": ""
70+
}
71+
}
72+
}

config/settings.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"reddit": {
3+
"subreddit": "subreddit",
4+
"username": "user",
5+
"password": "hunter4",
6+
"client_id": "client_id",
7+
"client_secret": "client_secret"
8+
},
9+
"db": {
10+
"host": "host",
11+
"port": 0000,
12+
"username": "user",
13+
"password": "hunter5",
14+
"database": "dbname"
15+
}
16+
}

file_manager.py

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# System imports
2+
import errno
3+
import io
4+
import json
5+
import os
6+
import sys
7+
# Project imports
8+
import log
9+
10+
11+
##############################
12+
# BASIC OPERATIONS
13+
##############################
14+
15+
# Saves string of text to a file
16+
def save(path, data_str, mode='w'):
17+
path = ensureAbsPath(path)
18+
try:
19+
with io.open(path, mode, encoding='utf-8') as f:
20+
f.write(str(data_str))
21+
except IOError:
22+
# This block of code will run if the path points to a file in a
23+
# non-existant directory. To fix this, create the necessary
24+
# director(y/ies) and call this function again to create the file.
25+
directory = os.path.dirname(path)
26+
if not os.path.isdir(directory):
27+
try:
28+
os.makedirs(directory)
29+
except OSError as oserr:
30+
pass
31+
if os.path.isdir(directory):
32+
return save(path, data_str, mode)
33+
else:
34+
log.error('Could not write to ' + path)
35+
return False
36+
else:
37+
return True
38+
39+
40+
# Reads text from a file as a string and returns it
41+
def read(path):
42+
path = ensureAbsPath(path)
43+
try:
44+
return open(path, encoding="utf8").read()
45+
except IOError:
46+
log.error('Could not find or open file: ' + path)
47+
return False
48+
49+
50+
# Deletes the file at the specified path. Cryptic, I know.
51+
def delete(path):
52+
path = ensureAbsPath(path)
53+
try:
54+
os.remove(path)
55+
except OSError:
56+
log.error('Could not remove file: ' + path)
57+
return False
58+
else:
59+
return True
60+
61+
62+
# If any relative paths are given, make them relative to the bot's working dir
63+
def ensureAbsPath(path):
64+
botRootDir = os.path.dirname(os.path.abspath(sys.argv[0])) + '/'
65+
return path if os.path.isabs(path) else botRootDir + path
66+
67+
68+
##############################
69+
# JSON FILES
70+
##############################
71+
72+
# Converts object to JSON, then saves it to a file
73+
def saveJson(path, data):
74+
return save(
75+
path,
76+
json.dumps(
77+
data,
78+
ensure_ascii=False,
79+
indent=4,
80+
separators=(',', ': ')
81+
)
82+
)
83+
84+
85+
# Reads text from a file, converts it to JSON, and returns it
86+
def readJson(path):
87+
f = read(path)
88+
if f:
89+
try:
90+
return json.loads(f)
91+
except ValueError:
92+
log.error('Could not parse JSON file: ' + path)
93+
return False
94+
else:
95+
return False
96+
97+
98+
##############################
99+
# MISC HELPER FUNCS
100+
##############################
101+
102+
# Appends data to a file rather than overwriting it -- useful for logging
103+
def append(path, data):
104+
return save(path, data + '\n', 'a')
105+
106+
107+
# Grabs the file contents and returns them, then deletes the file
108+
# This is primarily used for the temporary logging mechanism
109+
def readAndDelete(path):
110+
fileContents = read(path)
111+
delete(path)
112+
return fileContents

0 commit comments

Comments
 (0)