Initial Commit

This commit is contained in:
Dusty Pianalto 2019-10-19 10:10:52 -08:00
commit 03fd706642
10 changed files with 360 additions and 0 deletions

91
.gitignore vendored Normal file
View File

@ -0,0 +1,91 @@
config.json
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# mypy
.mypy_cache/
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/vcs.xml
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/dictionaries
.idea/**/shelf
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
cmake-build-release/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests

2
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
# Default ignored files
/workspace.xml

11
.idea/geeksbot-matrix.iml generated Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.7 (geeksbot-matrix)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (geeksbot-matrix)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/geeksbot-matrix.iml" filepath="$PROJECT_DIR$/.idea/geeksbot-matrix.iml" />
</modules>
</component>
</project>

25
.idea/watcherTasks.xml generated Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<TaskOptions isEnabled="true">
<option name="arguments" value="$FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="py" />
<option name="immediateSync" value="false" />
<option name="name" value="black formatter" />
<option name="output" value="$FilePath$" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="/usr/local/bin/black" />
<option name="runOnExternalChanges" value="true" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="false" />
<option name="workingDir" value="$ProjectFileDir$" />
<envs />
</TaskOptions>
</component>
</project>

43
geeksbot.py Normal file
View File

@ -0,0 +1,43 @@
import json
import asyncio
from nio import (AsyncClient, RoomMessageText)
import logging
log_format = '%(asctime)s ||| %(name)s | %(levelname)s | %(message)s'
logging.basicConfig(format=log_format, level=logging.INFO)
logger = logging.getLogger('geeksbot')
logger.setLevel(logging.INFO)
class Geeksbot(AsyncClient):
def __init__(self, base_url, username, password):
self.base_url = base_url
self.username = username
self.password = password
super(Geeksbot, self).__init__(self.base_url, self.username)
self.add_event_callback(self.on_message, RoomMessageText)
async def login(self):
await super(Geeksbot, self).login(self.password)
async def on_message(self, room, event):
logger.info(f'Message recieved for room {room.display_name} | {room.user_name(event.sender)}: {event.body}')
if event.body.startswith('$say '):
msg = {
"body": event.body.split(' ', 1)[1],
"msgtype": 'm.text'
}
print(msg)
resp = await self.room_send(room.room_id, 'm.room.message', msg)
print(resp)
async def main():
with open('config.json') as f:
config = json.load(f)
client = Geeksbot(**config)
await client.login()
await client.sync_forever(timeout=1000)
asyncio.get_event_loop().run_until_complete(main())

0
lib/__init__.py Normal file
View File

167
lib/http.py Normal file
View File

@ -0,0 +1,167 @@
import json
from typing import Union
import uuid
import aiohttp
from aiohttp.client_exceptions import ClientConnectionError
import asyncio
from urllib.parse import quote, urlencode, urlparse
from dataclasses import dataclass
MATRIX_API = "/_matrix/client/r0"
MATRIX_MEDIA = "/_matrix/media/r0"
@dataclass
class HTTPConfig:
max_retry: int = 10
max_wait_time: int = 3600
backoff_factor: float = 0.1
ssl: bool = None
proxy: str = None
class HTTP:
def __init__(
self,
*,
base_url: str,
username: str,
password: str = None,
token: str = None,
device_id: str = None,
device_name: str = None,
config: HTTPConfig = HTTPConfig(),
):
self.base_url = base_url
self.username = username
self.password = password
self.token = token
self.device_id = device_id
self.device_name = device_name
self.access_token = None
self.config = config
self.client_session = aiohttp.ClientSession()
def build_url(
self, endpoint: str, request_type: str = None, query: dict = None
) -> str:
path = f'{MATRIX_MEDIA if request_type == "MEDIA" else MATRIX_API}/{endpoint}'
path = self.base_url + quote(path)
if query:
path += f"?{urlencode(query)}"
return path
def get_wait_time(self, num_timeouts: int) -> float:
if num_timeouts <= 2:
return 0.0
return min(
self.config.backoff_factor * (2 ** (num_timeouts - 1)),
self.config.max_wait_time,
)
async def close(self):
if self.client_session:
await self.client_session.close()
self.client_session = None
async def _send(
self, method: str, path: str, data: dict = None, headers: dict = {}
) -> Union[dict, bytes]:
if not self.client_session:
self.client_session = aiohttp.ClientSession()
raw_resp = await self.client_session.request(
method,
path,
json=data,
ssl=self.config.ssl,
proxy=self.config.proxy,
headers=headers,
)
if raw_resp.content_type == "application/json":
return await raw_resp.json()
else:
return await raw_resp.read()
async def send(
self, method: str, path: str, data: dict = None, content_type: str = None
) -> dict:
if not self.access_token:
raise RuntimeError("Client is not logged in")
headers = {
"Authorization": f"Bearer {self.access_token}",
"content_type": content_type or "application/json",
}
timeouts = 0
for _ in range(self.config.max_retry or 1):
try:
resp = await self._send(method, path, data, headers)
if isinstance(resp, bytes):
break
if resp.get("retry_after_ms"):
await asyncio.sleep(resp["retry_after_ms"] / 1000)
else:
break
except (asyncio.TimeoutError, ClientConnectionError, TimeoutError):
timeouts += 1
await asyncio.sleep(self.get_wait_time(timeouts))
else:
raise RuntimeWarning(f"Max retries reached for {method} - {path} | {data}")
return resp
async def login(self):
path = self.build_url("login")
data = {}
if self.password:
data = {
"type": "m.login.password",
"identifier": {"user": self.username, "type": "m.id.user"},
"password": self.password,
}
elif self.token:
data = {"type": "m.login.token", "token": self.token}
if self.device_id:
data["device_id"] = self.device_id
if self.device_name:
data["device_name"] = self.device_name
headers = {"content_type": "application/json"}
resp = await self._send("post", path, data=data, headers=headers)
self.access_token = resp.get("access_token")
self.device_id = resp.get("device_id")
return resp
async def logout(self):
path = self.build_url("logout")
await self.send("POST", path)
self.access_token = None
async def logout_all(self):
path = self.build_url("logout/all")
await self.send("POST", path)
self.access_token = None
async def room_send(self, room_id: str, event_type: str, content: dict):
if room_id.startswith("!") and ":" in room_id:
path = self.build_url(f"rooms/{room_id}/send/{event_type}/{uuid.uuid4()}")
elif room_id.startswith("#") and ":" in room_id:
path = self.build_url(f"directory/room/{room_id}")
resp = await self.send("GET", path)
if resp.get("room_id"):
path = self.build_url(
f'rooms/{resp["room_id"]}/send/{event_type}/{uuid.uuid4()}'
)
else:
raise RuntimeWarning(resp)
else:
raise RuntimeWarning(f"{room_id} is not a valid room id or alias")
return await self.send("PUT", path, data=content)